💡 今日學習目標:理解 OWASP Top 10 第一名「權限控制失效 (Broken Access Control)」的攻擊手法(如 IDOR 越權存取),並掌握安全的權限驗證通則邏輯。
A01 - 權限控制失效 (Broken Access Control) 是 OWASP Top 10 的衛冕冠軍:2021 年登上第一之後,2025 年依然穩坐榜首,而且守備範圍還變得更大了(Day 15 提過,SSRF 在 2025 年被併進了這一類)。
它的發生率是 3.74%,在十大類別裡數一數二。
什麼是權限控制失效?簡單說就是:使用者做到了他本來不該做到的事。
而它有兩種常見的長相:

今天我們就把這兩種都親手拆一次。
IDOR(Insecure Direct Object References,不安全的直接物件參考)通常發生在系統把資料庫的內部主鍵(如 order_id = 1001)直接暴露在 URL、表單或 API Body 中,而後端沒有驗證這筆資料到底屬於誰。
攻擊成本低到誇張:把網址上的數字改一個就好。不需要工具、不需要技術,一個好奇的使用者就可能不小心發現。
// ❌ 漏洞邏輯:只拿前端傳進來的 orderId 去查資料庫,完全相信前端輸入
Function GetOrderDetail(req):
orderId = req.GetQueryParam("order_id")
// 直接根據 orderId 搜尋訂單(未驗證該訂單擁有人是誰)
Order = DB.Query("SELECT * FROM orders WHERE id = ?", orderId)
If Order == Null:
Return Error(404, "Order Not Found")
// 🚨 直接傳回!攻擊者只要修改 order_id,就能看遍全世界的訂單
Return JSONResponse(Order)
要防範 IDOR,核心通則有兩種做法:
// ✅ 安全邏輯 A:強制使用伺服器端 Session 中的 User ID
Function GetOrderDetail(req):
// 1. 從伺服器安全 Session/JWT 取得當前已驗證的使用者 ID (不可由前端篡改)
currentUserId = req.Session.GetUserId()
orderId = req.GetQueryParam("order_id")
// 2. 雙條件查詢:訂單 ID + 擁有人 ID
Order = DB.Query("SELECT * FROM orders WHERE id = ? AND owner_user_id = ?", orderId, currentUserId)
If Order == Null:
// 為了安全,不提示「權限不足」,統一提示「找無此資源」
Return Error(404, "Order Not Found")
Return JSONResponse(Order)
// ✅ 安全邏輯 B:顯式執行 Policy 權限檢查
Function GetOrderDetail(req):
currentUserId = req.Session.GetUserId()
orderId = req.GetQueryParam("order_id")
Order = DB.FindOrder(orderId)
// 顯式調用權限原則 (Policy Check)
If NOT AuthorizationPolicy.CanUserAccessOrder(currentUserId, Order):
AuditLog.Warn("Privilege Escalation Attempt by User: " + currentUserId)
Return Error(403, "Access Denied")
Return JSONResponse(Order)
🤔 你可能注意到了:一個回 404、一個回 403,到底該用哪個?
這其實是刻意的差別,判斷標準是:你願不願意讓對方知道「這個東西存在」?
- 回 404:適用於「連存在與否都不該讓他知道」的資源:別人的訂單、別人的檔案。回 403 等於親口告訴攻擊者「這筆訂單真的存在,只是不給你看」,他就可以拿來枚舉出所有有效的 ID。
- 回 403:適用於「他知道存在、只是沒權限」的功能,例如後台頁面。這時回 404 反而會讓正常使用者一頭霧水。
做法 A 因為是查別人的資料,所以用 404;做法 B 如果套用在後台功能上,403 就很合適。重點不是哪個數字對,而是別在錯誤訊息裡送出你不打算給的資訊。
平行越權是「看到別人的資料」,垂直越權則更嚴重:一般使用者執行了管理員才能做的操作。
它最常見的成因,是一句讓人背脊發涼的話:「反正前端不會顯示那顆按鈕。」
// ❌ 漏洞邏輯:後台 API 只靠「前端沒有畫出按鈕」來保護
Function DeleteUser(req):
targetUserId = req.GetParam("user_id")
DB.DeleteUser(targetUserId) // 完全沒有檢查呼叫者是誰、有沒有資格
Return Success()
⚠️ 攻擊者怎麼玩:他根本不需要你的前端。打開開發者工具看一眼網路請求、或直接猜
/api/admin/...這種常見路徑,然後用curl送出去就結束了。前端的按鈕從來就不是權限控制(這正是 Day 14 說的「前端驗證不是防線」)。
// ✅ 安全邏輯:在伺服器端明確檢查角色,且預設拒絕
Function DeleteUser(req):
currentUser = req.Session.GetUser()
// 預設拒絕(SEI 法則 5):不是管理員,一律擋下
If NOT currentUser.HasRole("Admin"):
AuditLog.Warn("Privilege escalation attempt", user=currentUser.Id, action="DeleteUser")
Return Error(403, "Forbidden")
targetUserId = req.GetParam("user_id")
DB.DeleteUser(targetUserId)
Return Success()
🏗️ 但更好的做法是:不要在每支 API 裡各寫一次。
把角色檢查放進 Middleware / Filter,讓「所有
/api/admin/*的路由預設都需要 Admin 角色」。這樣一來,就算未來有人新增了一支後台 API 卻忘了寫檢查,它也依然是被保護的。這就是 Day 13 講的 SEI 法則 3「為安全政策設計架構」:讓權限檢查有一個統一實施的地方,而不是分散在幾十個函式裡等著被漏掉。
很多人第一次聽到 IDOR,直覺反應是:把 order_id=1001 換成 UUID,攻擊者就猜不到了。
UUID 確實讓「亂猜」變得不可行,但它不是存取控制,只是把門牌號碼換難記一點而已。
因為 UUID 還是會外流:
而只要洩漏一次,沒有任何一層檢查會擋住他,因為你根本沒做檢查。
🛡️ 正確的定位:UUID 是很好的縱深防禦補強(Day 14 說的那疊防線之一),但它永遠不能取代擁有權驗證。用了 UUID,那句
WHERE owner_user_id = ?還是得寫。
user_id 只是一個字串,它證明不了任何事。真正的身分來自無法被篡改的 Session 或已驗簽的 JWT。WHERE id = ? AND owner_user_id = ?。少了後半句,就是一個 IDOR。/api/admin/*,比要求每個人「記得加檢查」可靠一百倍。💬 明日預告:【Day 17】【動手做】OWASP A04 加密機制失效:敏感資料保護與密碼雜湊
明天我們要處理的是:當資料庫真的被拖走的那一天,你的使用者密碼還撐不撐得住。